package com.ypy.context.beans;

import com.ypy.context.annotation.Component;
import com.ypy.context.annotation.DI;
import com.ypy.context.annotation.Pea;
import com.ypy.context.exception.InjectBeanException;
import com.ypy.context.exception.NotSupportMoreSuperInterface;
import com.ypy.context.utils.ArrayUtils;
import com.ypy.context.utils.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Map;

@Slf4j
public class DefaultListableApplicationContext extends AbstractApplicationContext {
	/**
	 * 扫描的基础路径
	 */
	private String basePath;

	public DefaultListableApplicationContext(String basePackage) {
		String packagePath = basePackage.replaceAll("\\.", "\\\\");
		try {
			URL url = Thread.currentThread().getContextClassLoader().getResource(packagePath);
			if (StringUtils.hasText(url.getPath())) {
				String filePath = URLDecoder.decode(url.getFile(), "UTF8");

				// populate basePath to use as root path
				basePath = filePath.substring(0, filePath.length() - packagePath.length());

				//scan base package path to load beans
				loadBeans(new File(filePath));

				// inject property for component
				loadDI();

				// store inner bean of component class
				loadInnerBeans(objectsTypeMap);
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}


	/**
	 * scan the specified file with file path, then get the beans into bean factory.
	 *
	 * @param file
	 */
	private void loadBeans(File file) {
		// 1. determines whether the file is a directory or not
		if (file.isDirectory()) {
			File[] childrenFiles = file.listFiles();
			// 2. fetch all files in the directory

			// 3. determine whether the directory is empty, if true,then directly return.
			if (ArrayUtils.<File>isEmpty(childrenFiles)) {
				return;
			}
			// 4. traverse the directory, if not empty, then traverse the directory
			for (File childFile : childrenFiles) {
				// 4.1  遍历得到每个file对象，继续判断。如果是文件夹，递归
				if (childFile.isDirectory()) {
					loadBeans(childFile);
				} else {
					// 4.2 not directory, that is a file.
					// 4.3 得到包路径 + 类名称部分
					// C:/desktop/IdeaProjects/spring-mytest/src/main/java/com/ypy/context
					String pathWithClass = childFile.getAbsolutePath().substring(basePath.length() - 1);
					// 4.4 判断当前文件类型是否为.class
					if (FileUtils.isClassFile(pathWithClass)) {
						// 4.5 如果是.class类型，把路径\替换成., 把.class去掉 （com.ypy.context.beans.UserServiceImpl)
						String allName = FileUtils.withoutClassSuffix(FileUtils.replaceBackslashWithDots(pathWithClass));

						// 4.6 instaniate the class by reflection.
						try {
							Class<?> clazz = Class.forName(allName);
							if (!clazz.isInterface()) {
								// 4.6.1 determines whether the class has @Component annotation, if so, then add it to the bean factory.
								if (clazz.getAnnotation(Component.class) != null) {

									Object instance = clazz.getConstructor().newInstance();

									// use the @Component annotation value as bean name first.
									if (StringUtils.hasText(clazz.getAnnotation(Component.class).value())) {
										String beanName = clazz.getAnnotation(Component.class).value();
										objectsMap.put(beanName, instance);
										log.warn(">>> objectsMap store bean(name,obj) ===> (" + beanName + "," + instance + ")");
										if (clazz.getInterfaces().length == 1) {
											objectsTypeMap.put(clazz.getInterfaces()[0], instance);
											log.warn(">>> objectsTypeMap store bean(class,obj) ===> (" + clazz.getInterfaces()[0].getSimpleName() + "," + instance + ")");
										} else if (clazz.getInterfaces().length == 0) {
											objectsTypeMap.put(clazz, instance);
											log.warn(">>> objectsTypeMap store bean(class,obj) ===> (" + clazz.getInterfaces()[0].getSimpleName() + "," + instance + ")");
										} else {
											throw new NotSupportMoreSuperInterface("Not support the bean that has more than two super interfaces.");
										}
									}

									// use parent interface's class name as bean name secondly.
									else if (clazz.getInterfaces().length > 0) {
										String interfaceName = clazz.getInterfaces()[0].getSimpleName();
										String beanName = lowercaseFirstLetter(interfaceName);
										objectsMap.put(beanName, instance);
										log.warn(">>> objectsMap store bean(name,obj) ===> (" + beanName + "," + instance + ")");
										// maybe it occurs bugs.
										objectsTypeMap.put(clazz.getInterfaces()[0], instance);
										log.warn(">>> objectsTypeMap store bean(class,obj) ===> (" + clazz.getInterfaces()[0].getSimpleName() + "," + instance + ")");
									}

									// If there doesn't exist parent interface, use class name as bean name.
									else {
										String beanName = lowercaseFirstLetter(clazz.getSimpleName());
										objectsMap.put(beanName, instance);
										log.warn(">>> objectsMap store bean(name,obj) ===> (" + beanName + "," + instance + ")");
										objectsTypeMap.put(clazz, instance);
										log.warn(">>> objectsTypeMap store bean(class,obj) ===> (" + clazz.getInterfaces()[0].getSimpleName() + "," + instance + ")");
									}
								}
							}
						} catch (Exception e) {
							throw new RuntimeException(e);
						}
					}

				}
			}

		}


	}

	/**
	 * scan the inner peas as objects ,then get them into bean factory.
	 *
	 * @param objectsTypeMap
	 */
	private void loadInnerBeans(Map<Class, Object> objectsTypeMap) {
		for (Map.Entry<Class, Object> entry : objectsTypeMap.entrySet()) {
			Class baseType = entry.getKey();
			Object localObj = entry.getValue();
			Class<?> localClass = localObj.getClass();
			String className = lowercaseFirstLetter(localClass.getSimpleName());
			for (Method method : localClass.getDeclaredMethods()) {
				method.setAccessible(true);
				Pea pea;
				String autoWiredBeanName = null;
				if ((pea = method.getAnnotation(Pea.class)) != null) {
					try {
						Class<?> returnType = method.getReturnType();
						autoWiredBeanName = lowercaseFirstLetter(returnType.getSimpleName());
						Object obj = method.invoke(getBean(className, localClass));

						if (StringUtils.hasText(pea.value())) {
							autoWiredBeanName = pea.value();
						}
						objectsMap.put(autoWiredBeanName, obj);
						log.warn("---------------------");
						objectsTypeMap.put(returnType, obj);
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				}
			}
		}
	}

	/**
	 * resolve all property for per bean in the bean factory.
	 */
	private void loadDI() {
		for (Map.Entry<String, Object> entry : objectsMap.entrySet()) {
			Object obj = entry.getValue();
			Class<?> clazz = obj.getClass();
			Field[] fields = clazz.getDeclaredFields();
			for (Field field : fields) {
				try {
					field.setAccessible(true);
					if (field.getAnnotation(DI.class) != null) {
						Class<?>[] interfaces = field.getType().getInterfaces();
						String needWiredBeanName;
						Object autoWiredBean;

						// Inject property using annotation DI's value firstly.
						if (StringUtils.hasText(needWiredBeanName = field.getAnnotation(DI.class).value())) {
							autoWiredBean = objectsMap.get(needWiredBeanName);
							if (autoWiredBean != null) {
								field.set(obj, autoWiredBean);
								log.warn("<<< DI: Class " + clazz.getSimpleName() + " of field named " + field.getName() + " is injected with value of " + autoWiredBean + " from " + "objectsMap, by value of annotation @DI");
								continue;
							}
						}

						// without parent interface.
						needWiredBeanName = lowercaseFirstLetter(field.getType().getSimpleName());
						if ((autoWiredBean = objectsMap.get(needWiredBeanName)) != null || (autoWiredBean = objectsTypeMap.get(field.getType())) != null) {
							field.set(obj, autoWiredBean);
							log.warn("<<< DI: Class " + clazz.getSimpleName() + " of field named " + field.getName() + " is injected with value of " + autoWiredBean + " , by value or type of property itself ");
							continue;
						}

						// search bean to inject as property from bean factory using name of parent interface.
						if (interfaces.length > 0) {
							for (Class<?> interfaceClazz : interfaces) {
								String interfaceClazzName = interfaceClazz.getSimpleName();
								if (interfaceClazz.isAssignableFrom(field.getType())) {
									needWiredBeanName = lowercaseFirstLetter(interfaceClazzName);
									if ((autoWiredBean = objectsMap.get(needWiredBeanName)) != null || (autoWiredBean = objectsTypeMap.get(interfaceClazz)) != null) {
										field.set(obj, autoWiredBean);
										log.warn("<<< DI: Class " + clazz.getSimpleName() + " of field named " + field.getName() + " is injected with value of " + autoWiredBean + ", by value or type of super interface");
									}

								}
							}
							continue;
						}
						throw new InjectBeanException("There occurs an Exception while injecting property filed [" + field.getName() + "] of Class <" + clazz.getSimpleName() + "> , because bean factory doesn't exist the bean named " + needWiredBeanName);
					}
				} catch (IllegalAccessException e) {
					throw new RuntimeException(e);
				}
			}

		}
	}

	/**
	 * lowercase the first letter of the string
	 *
	 * @param name the given string
	 * @return the string resolved lowercase first letter
	 */
	private String lowercaseFirstLetter(String name) {
		return name.substring(0, 1).toLowerCase() + name.substring(1);
	}

}
